Add gh auth token fallback before git credential fill#630
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refines APM’s GitHub authentication resolution by threading repository path context through auth lookups to reduce ambiguous multi-account credential selection, and introduces an explicit GitHub CLI (gh) fallback before invoking OS credential helpers.
Changes:
- Thread
repo_paththroughAuthResolverand pass it intogit credential fillrequests to disambiguate credential-helper lookups. - Add
gh auth token --hostname <host>as an earlier fallback in the GitHub token chain. - Update unit tests and authentication documentation to reflect the new resolution chain.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/core/auth.py |
Extends auth resolution/cache key with repo_path and adds gh/git fallback behavior. |
src/apm_cli/core/token_manager.py |
Adds repo-path aware git credential fill requests and a gh auth token fallback. |
src/apm_cli/commands/install.py |
Threads repo_path into try_with_fallback() during repo validation. |
src/apm_cli/marketplace/client.py |
Passes repo_path for marketplace fetches to improve credential selection. |
tests/unit/test_auth.py |
Adds a dep-aware test asserting repo-path is passed to credential fill. |
tests/test_token_manager.py |
Adds tests for gh fallback, path/username in credential fill, and cache key separation. |
packages/apm-guide/.apm/skills/apm-usage/authentication.md |
Updates the skill doc token precedence chain with gh + repo-path context. |
docs/src/content/docs/getting-started/authentication.md |
Updates public docs to describe repo-path context and the gh fallback step. |
sergio-sisternes-epam
left a comment
There was a problem hiding this comment.
Thanks for tackling this — multi-account credential ambiguity is a real pain point, and the gh auth token fallback is a genuinely great idea that we'd love to see land.
That said, we have concerns about the current scope. Auth resolution is our most security-sensitive code path, and this PR changes 8 files, alters cache key semantics from (host, org) to (host, org, repo_path), and threads a new input (repo_path) into git credential fill stdin — which is a protocol that interprets newlines as field delimiters. That's a large review surface for a security-critical area, and we need to be extra cautious here.
Our recommendation: land this incrementally.
The gh auth token --hostname <host> fallback alone solves ~90% of multi-account cases in a fraction of the code. It doesn't require repo_path threading, cache key changes, or new subprocess input — just a clean new step in the resolution chain.
We'd suggest:
- Slim this PR to just the
gh auth tokenfallback (beforegit credential fillin the chain). That's a focused, reviewable change we can merge with confidence. - If users later report issues in the "no
ghCLI, multiple GCM credentials" scenario, we can addrepo_pathpass-through togit credential fillas a follow-up PR — with proper input sanitization via ourpath_securityutilities.
Would you be open to scoping down to just the gh fallback? We're happy to help define the boundaries if useful.
Thanks again for the contribution — the core insight here is solid.
|
Thanks for taking the time to review the pull request and suggest modifications to be made. I'll look into reducing the scope of changes in this pull request this week |
|
Reduced the PR scope as requested in follow-up commit bcf42a4. This version keeps the GitHub CLI fallback (gh auth token --hostname ) ahead of git credential fill, and removes the repo-path threading, cache key expansion, and repo-path-specific tests/docs so the first batch stays focused. Focused auth validation still passes locally: ests/test_token_manager.py and ests/unit/test_auth.py. |
|
Hi @awakecoding -- friendly check-in: this PR has been in Concretely, the outstanding feedback is: Let us know either way -- a quick "still on it" or "going to close it" reply is enough. Thanks for the contribution! |
508ae43 to
16c1a87
Compare
|
Current branch scope is narrower than the original review summary described. The current PR diff is 6 files and no longer includes the repo-path work that prompted the earlier concern:
What remains in this PR is the GitHub CLI fallback before
Current changed files:
Focused validation still passes locally:
If the remaining 6-file diff still feels too broad, I can split out the doc/test-only follow-up separately, but the repo-context auth changes are already removed from this PR. |
Add a GitHub CLI fallback for GitHub-like hosts before invoking git credential fill. Update the auth resolution docs and focused auth tests to match the narrower fallback chain.
16c1a87 to
19ac1e2
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | Split guard logic: auth.py uses host_info.kind checks while token_manager uses _supports_gh_cli_host(); GHES resolution may diverge between the two paths. |
| CLI Logging Expert | 0 | 3 | 2 | Single log line covers 2 distinct fallback steps with no per-step outcome; gh-cli success/miss is invisible to operators and non-verbose users. |
| DevX UX Expert | 0 | 1 | 2 | Resolution order change is a net UX win; gh auth token is faster and non-interactive vs OS credential dialogs. One transparency gap in fallback logging. |
| Supply Chain Security Expert | 0 | 2 | 1 | gh CLI fallback is structurally safe: list-form subprocess prevents injection, FQDN guard constrains host, token scope is ambient by design. Two recommended hardening gaps. |
| OSS Growth Hacker | 0 | 2 | 1 | gh auth token fallback is a strong zero-config win; quickstart and README hero miss the 'works out of the box if you use gh' hook. |
| Auth Expert | 0 | 2 | 2 | Resolution order and precedence are correct; two behavioral gaps: gh-auth-token source not guarded in fallback (double-call), and _supports_gh_cli_host bypassed in auth.py paths. |
| Doc Writer | 1 | 3 | 1 | Docs are mostly accurate; one table-break bug in the skill file and two stale cells in the package source behavior table are the main issues. |
| Test Coverage Expert | 0 | 3 | 0 | Unit tests cover gh CLI subprocess plumbing well; three gaps: _supports_gh_cli_host untested, gh-auth-token source string never asserted, try_with_fallback gh-CLI branch unreachable from any test. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Doc Writer] (blocking-severity) Fix orphaned table row in apm-guide authentication.md by moving the prose note after the complete table -- Blocking rendering bug: the final
| -- | None |row is detached from the table by the inserted paragraph in all standard markdown parsers. One-line fix; must land before merge. - [Test Coverage Expert] Add unit tests for _supports_gh_cli_host (None/ADO/github.com/GHES/mismatch branches) and assert ctx.source == 'gh-auth-token' when gh CLI supplies the token -- Missing-outcome evidence on a secure-by-default surface. _supports_gh_cli_host gates whether gh CLI is ever invoked; the autouse disable fixture makes the try_with_fallback gh-CLI branch entirely unexercised.
- [Auth Expert] Add 'gh-auth-token' to _try_credential_fallback short-circuit guard and unify GHES guard logic between auth.py and token_manager.py -- Double subprocess invocation when gh-auth-token source fails, and latent GHES divergence between call-sites. Low blast radius but correctness defects on the auth critical path.
- [Supply Chain Security Expert] Add
stdin=subprocess.DEVNULLto the gh auth token subprocess call -- Cheap hardening: guarantees EOF on any interactive prompt attempt for older gh versions that ignore GH_PROMPT_DISABLED. One-line change, no behavior change for modern gh. - [OSS Growth Hacker] Fix authentication.md table priority-numbering inconsistency and add gh auth login callout to quickstart -- The table labeling gh auth token as priority 5 while prose says step 3 will confuse new users and undermine the zero-config narrative. Quickstart callout is the highest-conversion surface for the gh CLI cohort.
Architecture
classDiagram
direction LR
class AuthResolver {
<<Strategy>>
+resolve(host, org) AuthContext
+try_with_fallback(host, org, operation) T
-_resolve_token(host_info, org) tuple
-_try_credential_fallback(exc) T
}
class GitHubTokenManager {
<<ConcreteStrategy>>
+resolve_credential_from_gh_cli(host) str
+resolve_credential_from_git(host, port) str
+get_token_with_credential_fallback(purpose, host) str
-_supports_gh_cli_host(host) bool
-_is_valid_credential_token(token) bool
}
class HostInfo {
<<ValueObject>>
+host str
+kind str
+display_name str
}
class AuthContext {
<<ValueObject>>
+token str
+source str
+host_info HostInfo
}
class GitHubTokenManager:::touched
class AuthResolver:::touched
AuthResolver *-- GitHubTokenManager : delegates credential resolution
AuthResolver ..> HostInfo : reads kind/host
AuthResolver ..> AuthContext : produces
AuthContext *-- HostInfo : contains
note for AuthResolver "Chain of Responsibility:\nenv-PAT -> env-token -> gh-auth-token (NEW) -> git-credential"
note for GitHubTokenManager "_supports_gh_cli_host guard used only\nin get_token_with_credential_fallback;\nauth.py bypasses it with inline kind checks"
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["CLI: apm install / run"] --> B["AuthResolver.resolve(host, org)"]
B --> C["_resolve_token(host_info, org)"]
C --> D["Step 1: ADO_APM_PAT / per-org PAT env var"]
D -->|not found| E["Step 2: GITHUB_APM_PAT / GITHUB_TOKEN / GH_TOKEN"]
E -->|not found| F["Step 3 NEW: resolve_credential_from_gh_cli(host)"]
F --> G["subprocess: gh auth token --hostname host"]
G -->|token valid| H["return gh_token, source=gh-auth-token"]
G -->|fail/empty| I["Step 4: resolve_credential_from_git(host, port)"]
I --> J["subprocess: git credential fill"]
J -->|token found| K["return credential token"]
J -->|not found| L["return None -> unauthenticated"]
E -->|env token fails at remote| M["try_with_fallback detects failure"]
M --> N{"host_info.kind in github/ghe_cloud/ghes?"}
N -->|yes| O["resolve_credential_from_gh_cli(host)"]
O -->|token| P["retry operation with gh token"]
O -->|None| Q["resolve_credential_from_git fallback"]
N -->|no/ado| Q
Q -->|cred found| R["retry operation with git credential"]
Q -->|not found| S["raise original exception"]
style F fill:#fff3b0,stroke:#d47600
style O fill:#fff3b0,stroke:#d47600
sequenceDiagram
participant CLI as apm CLI
participant AR as AuthResolver
participant TM as GitHubTokenManager
participant GH as gh CLI subprocess
participant GIT as git credential subprocess
CLI->>AR: resolve(host, org)
AR->>AR: check PAT env vars (steps 1+2)
AR->>TM: resolve_credential_from_gh_cli(host)
TM->>GH: gh auth token --hostname host
GH-->>TM: token or non-zero exit
alt gh token valid
TM-->>AR: token
AR-->>CLI: AuthContext(source=gh-auth-token)
else no gh token
TM->>GIT: git credential fill
GIT-->>TM: credential or empty
TM-->>AR: credential or None
AR-->>CLI: AuthContext or unauthenticated
end
Recommendation
Merge after the doc-table break in apm-guide/.apm/skills/apm-usage/authentication.md is fixed (doc-writer blocking finding -- one-line move of the prose note after the complete table). The core auth behavior change is correct, the subprocess is structurally safe, and the zero-config story for gh CLI users is a net positive for APM adoption. File immediate follow-up issues for: (1) the three test-coverage gaps (autouse fixture disabling gh-CLI branch, missing _supports_gh_cli_host tests, missing source-string assertion), (2) the GHES guard unification and double-invocation fix in auth.py, and (3) the authentication.md table numbering fix and quickstart callout. The CHANGELOG entry should be framed as a user promise ('if you use gh, you are already set up') and a social beat should follow the merge.
Full per-persona findings
Python Architect
- [recommended] Divergent gh-CLI guard logic between auth.py and token_manager.py at
src/apm_cli/core/token_manager.py:104
auth.py gatesresolve_credential_from_gh_cliwithhost_info.kind in ('github','ghe_cloud','ghes')whiletoken_manager.get_token_with_credential_fallbackuses_supports_gh_cli_host(host)which checksdefault_host()equality for GHES. For a GHES host not matching the gh CLI's configured default_host(), auth.py will attempt gh auth token while token_manager skips it -- latent correctness divergence.
Suggested: Move the eligibility guard insideresolve_credential_from_gh_cliso all callers share one path; auth.py callers can then call it unconditionally and let it return None for ineligible hosts. - [nit] _supports_gh_cli_host is a private static used in only one of three call sites at
src/apm_cli/core/token_manager.py:104
Intended as a shared guard but auth.py bypasses it; consider folding its logic intoresolve_credential_from_gh_cliand removing the standalone method. - [nit] resolve_credential_from_gh_cli is
@staticmethodbut callsGitHubTokenManager._get_credential_timeout()by class name atsrc/apm_cli/core/token_manager.py:188
Consistent but couples the static to the concrete class; document the coupling if not extracted.
CLI Logging Expert
- [recommended] One log line covers both gh-cli and git-credential-fill steps with no per-step outcome logged at
src/apm_cli/core/auth.py
The single_log()fires before either fallback is attempted; user never knows which step succeeded or was skipped. Emit a second_log()after each sub-step outcome. - [recommended] 'trying fallback credentials' is too vague -- names neither step nor mechanism at
src/apm_cli/core/auth.py
The old message at least named 'git credential fill'. The new generic phrase hides both mechanisms; operators cannot diagnose auth failures from verbose output.
Suggested: Split into mechanism-specific messages: 'trying gh auth token for X' before the gh call, 'trying git credential fill for X' before the git call. - [recommended] All auth fallback messages are verbose_callback-only; non-verbose users get zero signal on auth renegotiation at
src/apm_cli/core/auth.py
When gh auth token silently recovers a failing install, the user sees no indication auth was renegotiated. - [nit] No timeout-exceeded or subprocess-error log path in resolve_credential_from_gh_cli at
src/apm_cli/core/token_manager.py
Exception swallowed silently; in --verbose mode a timed-out gh auth token call leaves no trace. - [nit] Docstring on _try_credential_fallback still says 'git-credential-fill' but function now covers gh CLI too at
src/apm_cli/core/auth.py
DevX UX Expert
- [recommended] Fallback log message lost specificity -- users cannot tell which mechanism was attempted at
src/apm_cli/core/auth.py
Before: 'trying git credential fill for X'. After: generic 'trying fallback credentials for X'. When auth fails after both fallbacks, developer cannot determine whether gh CLI was absent/not-logged-in vs git credential returned nothing.
Suggested: Emit a distinct log line per step inside each branch. - [nit] Prose line inserted mid-table breaks markdown rendering in skills authentication doc at
packages/apm-guide/.apm/skills/apm-usage/authentication.md
The new explanatory sentence is inserted between table rows and the closing '| -- | None |' row.
Suggested: Move the prose paragraph to after the closing table row. - [nit] No user-visible signal when gh auth token is the credential source
In a multi-account gh setup (gh auth switch), silent account confusion possible; a debug note citing source='gh-auth-token' at --verbose would let users verify intent.
Supply Chain Security Expert
- [recommended] gh auth token returns full-scope OAuth token with no scope narrowing at
src/apm_cli/core/token_manager.py
Token scopes set at login time (often 'repo' or 'read:packages'). APM cannot request minimal scope at retrieval time. In CI where operator expects only read:packages, a personal gh login active in the environment could silently elevate privilege.
Suggested: Document in auth docs that gh-cli fallback tokens inherit whatever scope the user granted at 'gh auth login' time, and log a debug notice (without the token) citing source='gh-auth-token'. - [recommended] GH_PROMPT_DISABLED=1 not guaranteed to suppress all interactive prompts on older gh versions at
src/apm_cli/core/token_manager.py
Older gh ignores this env var and may block on a TTY prompt until the timeout fires.
Suggested: Addstdin=subprocess.DEVNULLto the subprocess.run call -- guarantees EOF on any prompt attempt, making the timeout a last resort. - [nit] stderr from gh auth token silently swallowed with no debug trace at
src/apm_cli/core/token_manager.py
On non-zero exit, emitlogger.debug('gh auth token failed for %s: %s', host, result.stderr.strip()[:200])before returning None.
OSS Growth Hacker
- [recommended] Quickstart never mentions that gh auth login is all most users need -- missed conversion hook at
docs/src/content/docs/getting-started/quick-start.md
The PR makes gh auth login the zero-config path for private package users. A one-liner callout ('Already use the gh CLI? You are already authenticated.') prevents the drop-off that happens when the first private install returns a 401. - [recommended] authentication.md table inconsistency: prose says step 3 is gh auth, table labels it priority 5 at
docs/src/content/docs/getting-started/authentication.md
A new user reading the table will think env vars always win over gh CLI, creating confusion when GH_TOKEN set by Actions shadows the interactive gh session.
Suggested: Reconcile by renumbering the prose list to match the 6-row table, or add a note clarifying priorities 2-4 are all 'global env vars' collapsed to one step in the prose. - [nit] Doc update sentence buries user benefit -- lead with the outcome, trail with the mechanism.
Suggested: 'No extra tokens needed if you use the gh CLI -- APM automatically uses your active gh auth login account for github.com.'
Auth Expert
- [recommended] If gh-auth-token is the resolved source and it fails, _try_credential_fallback calls gh CLI again before git-credential-fill at
src/apm_cli/core/auth.py:360
_try_credential_fallback short-circuits only for sources 'git-credential-fill' and 'none'. If _resolve_token returned source='gh-auth-token' and that token is rejected by the remote, _try_credential_fallback invokes resolve_credential_from_gh_cli a second time, gets the same token, fails again, then falls through to git-credential-fill. Wastes one subprocess call and produces a confusing log.
Suggested: Add 'gh-auth-token' to the short-circuit guard. - [recommended] _supports_gh_cli_host GHES guard is bypassed in auth.py; both call-sites use host_info.kind directly at
src/apm_cli/core/auth.py:703
_supports_gh_cli_host checks default_host() for custom GHES hosts but auth.py's _resolve_token and _try_credential_fallback bypass it with kind checks. For GHES hosts not matching the gh CLI's configured default_host(), this causes an extra subprocess call that always fails.
Suggested: Route through _supports_gh_cli_host in both auth.py call-sites, or document the intentionally looser kind-based guard and remove _supports_gh_cli_host to eliminate drift. - [nit] gh auth token returns active account's token; multi-account gh setups may silently use wrong-org credentials at
src/apm_cli/core/token_manager.py:188
Suggested: Add docstring note: 'In multi-account setups the active account may not have access to the target org; use GITHUB_APM_PAT_{ORG} to pin a specific account.' - [nit] GH_NO_UPDATE_NOTIFIER not set alongside GH_PROMPT_DISABLED at
src/apm_cli/core/token_manager.py:201
Suggested:env={**os.environ, "GH_PROMPT_DISABLED": "1", "GH_NO_UPDATE_NOTIFIER": "1"}
Doc Writer
- [blocking] Prose note in apm-guide authentication.md breaks the table -- the '| -- | None |' row is orphaned below the inserted paragraph at
packages/apm-guide/.apm/skills/apm-usage/authentication.md
Markdown parsers will not render the final row as part of the table; it renders as literal pipe characters or a broken one-cell row.
Suggested: Move the '| -- | None | -- | Unauthenticated (public GitHub repos only) |' row to immediately after row 6, then place the explanatory prose after the complete table. - [recommended] Package source behavior table omits gh auth token step from the Auth behavior column at
docs/src/content/docs/getting-started/authentication.md
The table still reads 'Global env vars -> credential fill' for GitHub-like hosts, no longer reflecting the updated chain.
Suggested: Update the Auth behavior cells for github.com, *.ghe.com, and GHES rows to read 'Global env vars -> gh auth token -> credential fill'. - [recommended] No guidance on behavior when gh CLI is not installed -- silent skip vs. error? at
docs/src/content/docs/getting-started/authentication.md
Suggested: Add: 'If the gh CLI is not installed or no account is active, APM skips this step silently and continues to git credential fill.' - [recommended] Mermaid diagram fallback branch does not show that gh auth token step is GitHub-like hosts only at
docs/src/content/docs/getting-started/authentication.md
Suggested: Annotate the F node: 'gh auth token? (GitHub-like hosts)'. - [nit] Troubleshooting section default timeout differs between the two files (60s in authentication.md, 30s in apm-guide skill) at
packages/apm-guide/.apm/skills/apm-usage/authentication.md
Test Coverage Expert
- [recommended] _supports_gh_cli_host has no tests; its routing logic gates whether gh CLI is ever invoked at
src/apm_cli/core/token_manager.py
Grepped tests/ for '_supports_gh_cli_host' -- zero hits. Non-trivial branching: None/empty->False, github.com->True, GHES FQDN matching default_host()->True, ADO->False. Silent regression risk for GHES enterprise users.
Proof (missing):tests/test_token_manager.py-- proves: gh CLI is invoked for GitHub hosts and GHES custom FQDNs, skipped for None/ADO/mismatched hosts [secure-by-default] - [recommended] No test asserts ctx.source == 'gh-auth-token' when gh CLI supplies the token at
src/apm_cli/core/auth.py
Grepped all test files for 'gh-auth-token' -- zero hits. The string is emitted by auth.py _resolve_token and propagated into AuthContext.source. Drift would be invisible.
Proof (missing):tests/unit/test_auth.py-- proves: When gh CLI returns a token, AuthContext.source is 'gh-auth-token' [secure-by-default] - [recommended] try_with_fallback gh-CLI branch in auth.py is unreachable from every existing test at
src/apm_cli/core/auth.py
The autouse _disable_gh_cli_fallback fixture in test_auth.py patches resolve_credential_from_gh_cli to None for ALL tests. The code path (gh_token, 'gh-auth-token', 'basic') in auth.py is never executed.
Proof (missing):tests/unit/test_auth.py::TestTryWithFallback::test_gh_cli_token_used_when_no_env_token_github_host-- proves: try_with_fallback returns gh-auth-token credential when gh CLI succeeds on a GitHub host [secure-by-default]
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Note
🔒 Integrity filter blocked 2 items
The following items were blocked because they don't meet the GitHub integrity level.
- #630
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved". - Add gh auth token fallback before git credential fill #630
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by PR Review Panel for issue #630 · ● 3M · ◷
Code:
- Fold gh-CLI eligibility into resolve_credential_from_gh_cli; both
call sites now share _supports_gh_cli_host semantics, eliminating
the GHES-default-host divergence between auth.py and token_manager.
- Harden gh subprocess: stdin=DEVNULL, GH_NO_UPDATE_NOTIFIER=1, and
debug-log stderr on non-zero exit so --verbose users can see why
the call missed.
- Add 'gh-auth-token' to _try_credential_fallback short-circuit set
to prevent double-invocation when the original token came from gh.
- Per-step verbose logging in fallback chain ('trying gh auth token
for X' then 'trying git credential fill for X') and updated
docstring to cover gh CLI.
Tests (3 new gaps from review):
- TestSupportsGhCliHost: None/empty/ADO/github.com/*.ghe.com/GHES
match/GHES mismatch/no GITHUB_HOST.
- test_gh_cli_source_label: asserts AuthContext.source ==
'gh-auth-token' when gh CLI supplies the token.
- test_try_with_fallback_uses_gh_cli: exercises the gh-CLI branch
through try_with_fallback, escaping the autouse disable fixture.
- Augmented existing TestResolveCredentialFromGhCli with assertions
for stdin=DEVNULL and GH_NO_UPDATE_NOTIFIER=1, plus a guard test
that ineligible hosts skip the subprocess entirely.
Docs:
- packages/apm-guide/.apm/skills/apm-usage/authentication.md: move
the orphaned '| -- | None |' row inside the table (was after the
prose, breaking the table) and place the Note paragraph after the
complete 7-row table. Add silent-skip behavior note.
- docs/.../getting-started/authentication.md: reconcile prose-vs-
table priority numbering (prose collapses 3 env vars into one
step), update Package source behavior table to mention gh auth
token, annotate mermaid F node 'GitHub-like hosts only', add
silent-skip note.
- docs/.../getting-started/quick-start.md: add ':::tip' callout
near the install step for gh CLI users.
- Reconciled stale '30s' timeout drift to '60s' to match
DEFAULT_CREDENTIAL_TIMEOUT.
CHANGELOG: framed as user promise under [Unreleased] -> Added,
tagged (microsoft#630).
Verified: ruff check + format silent; 140 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Acted on the panel review in commit 74d4e78 (pushed to Blocking (doc-writer)
Code (auth-expert)
Tests (test-coverage-expert -- 3 gaps)
Docs (devx-ux + oss-growth-hacker)
CHANGELOG
Verified
|
…egression trap (#1226) * fix(auth): per-URL credential disambiguation + gh-cli short-circuit regression trap PR #630 added a gh CLI fallback to sidestep Git Credential Manager's multi-account picker, but it did not actually disambiguate credentials per repository URL. APM was sending only protocol/host to 'git credential fill', so GCM had no path to match against 'credential.https://github.com/<org>.username' config and would still prompt when 'gh' was unavailable. This change: * Plumbs an optional 'path' through GitHubTokenManager.resolve_credential_from_git and AuthResolver.try_with_fallback. When set, APM appends 'path=<org/repo>' to the credential-fill stdin so GCM users with 'git config --global credential.useHttpPath true' get the right account per URL without a prompt. * Wires path at the three call sites that already know it: the install validation API check, the validation parse-failure fallback, and the marketplace client fetch. * Adds a regression trap (unit + integration) asserting that 'git credential fill' is NOT invoked when gh CLI returns a token. This guards the silent-drift gap left by PR #630: a refactor that re-orders the chain (or accidentally calls credential fill unconditionally) would silently re-introduce the prompt for users who configured gh. Documentation updated with a new 'Multi-account Git Credential Manager' section explaining the useHttpPath workflow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(changelog): backfill PR number after opening #1226 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #1226 review: sanitize credential path; urlparse assertion; doc polish - token_manager: sanitize path before appending to git credential fill stdin. Reject control chars / whitespace (defense-in-depth against newline-injected attribute lines). New unit tests cover \n, \r, and whitespace; the malformed input is dropped silently and the request falls back to host-only matching. - integration test: replace url.endswith() with urlparse(...).path comparison per the repo's CodeQL convention (py/incomplete-url-substring-sanitization). - authentication.md: shorten the row-6 'Notes' cell back to its original concise form; the dedicated 'Multi-account Git Credential Manager' section below explains path forwarding. Rephrase the closing sentence per the current-behaviour doc convention. - CHANGELOG: tighten the #1226 entry to a single concise line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #1226 panel review: urlparse guard, integration tests, doc polish Bundles the panel's recommended-tier follow-ups with the already-shipped newline/CR/NUL sanitization (commit da020ec): - token_manager: bundle python-architect's urlparse URL-format guard with the sanitizer so a future caller passing a full URL gets the URL's path component (not the naive lstrip-yields-https:/...). New unit test covers the URL extraction path. - auth.py: surface path= in the verbose 'trying git credential fill' log so multi-account GCM debugging is diagnosable (cli-logging recommendation). - validation.py:524: clarifying comment on the dep_ref.repo_url invariant (owner/repo, never a full URL) -- prevents future call-site confusion. - integration tests: add two coverage floors flagged as secure-by-default by test-coverage-expert and auth-expert: test_credential_fill_receives_path_on_parse_failure_fallback covers validation.py:590 _check_repo_fallback; test_marketplace_fetch_threads_path_to_credential_fill exercises the marketplace -> try_with_fallback -> credential fill chain end-to-end. - packages/apm-guide/.apm/skills/apm-usage/authentication.md: fix the agent-facing skill drift -- row 6 now reflects path= forwarding and cross-references the main authentication guide. - docs/.../authentication.md: lead the multi-account GCM section with the user outcome (hook line) and the backward-compat reassurance; add the GCM prefix-match clarification (devx-ux recommendation) so users know one entry per org typically suffices; drop the gh-cli paragraph that restated the precedence chain (PROSE: state once). - CHANGELOG: reframe the entry as a user promise (oss-growth recommendation): 'APM now selects the right GitHub account automatically per repository ... existing single-account setups are unaffected.' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #1226 panel-review followups: sanitizer tests, scheme allowlist, repo_path guard Five recommended-tier follow-ups from the second panel pass: 1. token_manager: tighten _sanitize_credential_path to allowlist schemes (https/http/ssh + schemeless); reject data: / file: / javascript: which would otherwise let urlparse silently strip the embedded payload past the char-scan. Schemes are normalized to lowercase before the allowlist check. 2. tests/test_token_manager.py: new TestSanitizeCredentialPath with parametrized direct coverage of all four code paths -- LF/CR/NUL/ tab/whitespace/DEL rejection, scheme allowlist (case-insensitive), full-URL extraction, and valid passthrough. 3. validation.py: defensive owner/repo regex guard on the parse-failure fallback path. Anything that isn't [A-Za-z0-9._-]+/[A-Za-z0-9._-]+ short-circuits to False before either the API URL or git credential fill path= sees the value -- defends GHES deployments against path-confusion sequences in the parse-failure branch. 4. auth.py: inline comment at _resolve_token's credential-fill site documenting why path= is intentionally omitted there (one wasted round-trip on first-miss vs. threading repo context through every resolve() caller). 5. authentication.md: qualify the GCM prefix-match claim with '(v2.1+)' and note that other helpers may require an exact path match. Add a 'Seeing an account picker mid-install?' troubleshooting callout covering the three diagnostic steps (config check, urlmatch lookup, --verbose log line APM emits). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Daniel Meppiel <copilot-rework@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve conflicts between PR microsoft#1149 (GitLab marketplace host support) and main's recent refactors (microsoft#1223 HostBackend Protocol, microsoft#630 gh CLI fallback). - Add GitLabBackend (kind='gitlab', is_generic=True) embedding PAT via oauth2:token@host using build_gitlab_https_clone_url, registered in _BACKEND_BY_KIND so backend_for routes gitlab.com correctly. - download_strategies.build_repo_url: adopt main's backend dispatch and add a GitLab branch resolving the per-dep PAT through auth_resolver.resolve_for_dep. - github_downloader: keep main's delegating refactor (GitAuthEnvBuilder, CloneEngine, GitReferenceResolver); merge debug log to include has_gitlab_token. - auth.py / token_manager.py docstrings: reflect new chain ordering (env vars -> gh CLI -> credential helper) for GitHub class plus GitLab and Generic entries. - authentication.md: per-host-class chain narrative, table, package source table and mermaid flowchart updated to include gh CLI step. - CHANGELOG: keep both Added entries (GitLab + gh CLI fallback). - Update test_dispatch_generic to use a non-GitLab generic host and add test_dispatch_gitlab covering the new backend dispatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
This PR is intended to disambiguate GitHub authentication based on the repository URL when APM resolves credentials for package fetches.
The motivating issue is Git Credential Manager account-selection prompts on machines that use more than one GitHub account. In my case, APM was attempting authentication in a way that was not specific enough to the target repository, which caused GCM to ask which account to use.
With these changes, I was able to configure different default user accounts for different GitHub repository URLs and use APM to fetch from private repositories without getting the account-selection prompt.
What changed
Why this exists
The goal is to avoid ambiguous host-only credential resolution for GitHub package fetches, especially on developer machines that are signed into multiple GitHub accounts.
The key idea is that the repository URL contains enough context to help select the correct identity, and in practice these changes worked for me.
Notes for reviewers
These patches were vibe coded while investigating the issue end to end. Please feel free to close this PR in favor of a different implementation if the same behavior should be achieved another way.
If the changes are acceptable as-is, great. If they require adjustments, I can make them.